package jam4j;

import java.io.File;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.*;
import java.util.concurrent.CountDownLatch;

/**
 * Holder for all global variables. Note that Jam4J does its best to isolate
 * itself from its environment; therefore "global" here means "global within
 * the running Jam4J instance," which may have been launched from the command
 * line or a unit test or an Ant task or something else. This way we can run
 * under an alternate working directory, or with different stdin/stdout/stderr
 * streams, or whatever, without worrying about the rest of the JVM.
 * 
 * @author Luke Maurer
 */
public final class Globals {
    static final InheritableThreadLocal<Globals> INSTANCE =
        new InheritableThreadLocal<Globals>() {
            protected Globals initialValue() {
                return new Globals();
            }
        };
    
    InputStream in = System.in;
    PrintStream out = System.out, err = System.err, cmdout;
    File cwd = new File(System.getProperty("user.dir"));
    Date startupTime = new Date();
    boolean allTargets, noExec, quitQuick;
    int jobs = 1;
    EnumSet<OutputChannel> enabledChannels =
        EnumSet.noneOf(OutputChannel.class);
    
    private volatile boolean intr;
    private final CountDownLatch end = new CountDownLatch(1);
    
    private static Globals instance() {
        return INSTANCE.get();
    }
    
    /**
     * Get the global input stream.
     * 
     * @return The stream to use as if it were {@link System#in}.
     * 
     * @see Jam4J#inputStream(InputStream)
     */
    public static InputStream in()      { return instance().in; }
    /**
     * Get the global output stream.
     * 
     * @return The stream to use as if it were {@link System#out}.
     * 
     * @see Jam4J#outputStream(PrintStream)
     */
    public static PrintStream out()     { return instance().out; }
    /**
     * Get the global error stream.
     * 
     * @return The stream to use as if it were {@link System#err}.
     * 
     * @see Jam4J#errorStream(PrintStream)
     */
    public static PrintStream err()     { return instance().err; }
    /**
     * Get the command output stream.
     * 
     * @return The stream to which commands to be executed are printed.
     */
    public static PrintStream cmdout()  { return instance().cmdout; }
    /**
     * Get the working directory. This may not be the "official" one.
     * 
     * @return The working directory.
     */
    public static File cwd()            { return instance().cwd; }
    /**
     * Get the time at startup.
     * 
     * @return The time at which Jam4J was launched.
     */
    public static Date startupTime()    { return instance().startupTime; }
    /**
     * Get the user-provided job limit.
     * 
     * @return The maximum number of jobs allowed to run concurrently.
     * 
     * @see Jam4J#jobs(int)
     */
    public static int jobs()            { return instance().jobs; }
    /**
     * Find whether we're forcing all targets to be built.
     * 
     * @return Whether to build all targets, whether or not they're out of
     *         date.
     *         
     * @see Jam4J#allTargets()
     */
    public static boolean allTargets()  { return instance().allTargets; }
    /**
     * Find whether we're not actually executing anything.
     * 
     * @return Whether to suppress actual execution of update actions.
     * 
     * @see Jam4J#noExec()
     */
    public static boolean noExec()      { return instance().noExec; }
    /**
     * Find whether we quit at first error.
     * 
     * @return Whether to quit as soon as the first update action fails.
     * 
     * @see Jam4J#quitQuick()
     */
    public static boolean quitQuick()   { return instance().quitQuick; }
    
    /**
     * Find whether we've been interrupted.
     * 
     * @return Whether an update action has been interrupted by the SIGINT
     * signal.
     */
    public static boolean intr()        { return instance().intr; }
    
    /**
     * Interrupt execution because a SIGINT or similar has been received.
     */
    public static void interrupt() {
        instance().intr = true;
    }
    
    /**
     * Whether the given output channel is enabled.
     * 
     * @param channel The channel to check.
     * @return True if the channel is enabled.
     * 
     * @see Jam4J#display(Jam4J.DisplayOption)
     * @see Jam4J#display(int, boolean)
     */
    public static boolean enabled(OutputChannel channel) {
        return instance().enabledChannels.contains(channel); 
    }
    
    /**
     * Wait until execution is complete and the main thread is exiting. 
     * 
     * @throws InterruptedException If the thread is interrupted.
     */
    public static void awaitEnd() throws InterruptedException {
        instance().end.await();
    }
    
    private static final InheritableThreadLocal<Deque<Globals>> NESTED =
        new InheritableThreadLocal<Deque<Globals>>() {
            protected Deque<Globals> initialValue() {
                return new ArrayDeque<Globals>();
            }
        };
    
    static void push(Globals globals) {
        NESTED.get().push(INSTANCE.get());
        INSTANCE.set(globals);
    }
    
    static Globals pop() {
        final Globals old = INSTANCE.get();
        if (NESTED.get().isEmpty())
            INSTANCE.remove();
        else
            INSTANCE.set(NESTED.get().pop());
        old.end.countDown();
        return old;
    }
    
    void enable(OutputChannel channel) {
        if (channel != null)
            enabledChannels.add(channel);
    }
    
    void enable(int level) {
        enable(OutputChannel.LEVELS[level]);
    }
}